package com.mobilottery.simulation;

import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.regex.Pattern;

import org.apache.log4j.Logger;

import com.mobilottery.Constants;
import com.mobilottery.external.ILotteryHandler;
import com.mobilottery.external.ILotterySystem;
import com.mobilottery.server.Game;
import com.mobilottery.server.GameParticipation;
import com.mobilottery.server.PersistenceManager;

/**
 * Lottery system simulator. Simulates game handling and updates of existing games by callback @see com.mobilottery.external.ILotterySystem 
 * @author fedor
 */
public class LotterySystemSimulator implements ILotterySystem 
{
	private static Logger logger = Logger.getLogger(LotterySystemSimulator.class);
	private ILotteryHandler lotteryHandler;	
	private Timer scheduleTimer = new Timer();
	private Date nextUpdateDate = null;
		
	private static final Pattern SAMPLE_CREDIT_CARD_PATTERN  = Pattern.compile("(\\d+\\-+)+(\\d+\\-*)+"); 

	/*
	 * (non-Javadoc)
	 * @see com.mobilottery.external.ILotterySystem#checkCreditCard(java.lang.String)
	 */
    public String checkCreditCard(String creditCardNumber) 
    {    	
        if(!SAMPLE_CREDIT_CARD_PATTERN.matcher(creditCardNumber).matches())
        {
        	return "invalid credit card (check if it is in the format 1234-5678)";
        }
        return "";
    }

    /*
     * (non-Javadoc)
     * @see com.mobilottery.external.ILotterySystem#participateInGame(java.lang.Long, java.lang.Long, java.lang.String, java.util.List)
     */
    public String participateInGame(Long gameId, Long userId,  String creditCardNumber, Set<Long> numbers) 
    {
        Logger.getLogger(getClass()).debug("register participation userId:" + userId + ", gameId: " + gameId);
    	//we use the same data for simulation, so only need to create a transaction id
        //the participation will be stored by game provider and we'll fetch it when updating results 
        return Long.toString((long)(Math.random()*Long.MAX_VALUE));
    }
    
    /*
     * (non-Javadoc)
     * @see com.mobilottery.external.ILotterySystem#setLotteryHandler(com.mobilottery.external.ILotteryHandler)
     */
    public void setLotteryHandler(ILotteryHandler handler) 
    {
    	if(lotteryHandler!=null) throw new RuntimeException("simulator does not allow change of lottery handler");
		lotteryHandler = handler;		
		logger.info("initializing game simulator");
		updateNextSchedule();
    }
    
    /**
     * Frees simulator resources
     */
    public void destroy()
    {
		logger.info("shutting down game simulator");
		scheduleTimer.cancel();
    }

    
    
    /**
     * Fetches and updates all finished games
     */
    private void updateFinishedGames()
    {
    	try{
    	logger.debug("updating finished games");    	
        String hql = "select g from Game g where g.resultDate <= :now and g.winningNumbersRaw IS NULL ORDER BY g.resultDate ASC";
        //create parameters
        Map<String, Object> params = new HashMap<String, Object>();
        params.put("now", new Date());
        List<Game> finishedGames = PersistenceManager.getInstance().selectParamQuery(hql, params);
        if(finishedGames.size()>0)
        {
        	logger.debug("found " + finishedGames.size() + " game(s) to update");    	
        	for(Game game : finishedGames)
        	{
        		//create some winning number
        		Set<Long> winningNumber = new HashSet<Long>(6); 
        		while(winningNumber.size()<6)
        		{
        			long num = (long)(Math.random()*49 + 1);
        			winningNumber.add(num);
        		}

        		//simulated data
        		List<Long> winingTransactions = new ArrayList<Long>();
        		List<Float> winingAmounts = new ArrayList<Float>();

        		
        		//fetch all game participations for this game
        		List<GameParticipation> gameParticipations = game.getGameParticipations();
            	logger.debug("found " + gameParticipations.size() + " participation(s) to update from game " + game.getId());
            	
            	//add some winning number
            	if(!gameParticipations.isEmpty())
            	{
            		Long someNum = gameParticipations.get(0).getGuessedNumbers().iterator().next();
            		if(!winningNumber.contains(someNum))
            		{
            			winningNumber.remove(winningNumber.iterator().next());
            			winningNumber.add(someNum);
            		}
        		
	        		//fill winning transactions
	        		for(GameParticipation gameParticipation : gameParticipations)
	        		{
	        			//find correctly guessed numbers
	        			Set<Long> guessedNumbers = gameParticipation.getGuessedNumbers();
	        			List<Long> correctNumbers = new ArrayList<Long>();
	        			for(Long guessedNumber : guessedNumbers)
	        				if(winningNumber.contains(guessedNumber))
	        				{
	        					correctNumbers.add(guessedNumber);		
	        				}
	
	
	        			//store wining transaction and calculate winning amount
	        			if(!correctNumbers.isEmpty())
	        			{
	            			//store transaction id
	        				winingTransactions.add(gameParticipation.getTransactionId());
	            			//simulate winning as percentage of max prize rounded to tenth
	            			//(realistic simulation will include amount of users but we don't)
	            			winingAmounts.add( Float.parseFloat( new DecimalFormat("#.##").format(correctNumbers.size()*game.getMaxPrize()/6.0f)));
	        			}
	        				        			
	        			
	        		}     
            	}
        		lotteryHandler.updateGameResults(game.getExternalId(), winingTransactions, winingAmounts, winningNumber);
        	}
        }
        else
        {
        	logger.warn("scheduled update: no finished games found");
        }
    	}
    	finally
    	{
    		//close session
    		if(PersistenceManager.isInstanceInitialized()) PersistenceManager.closeInstance();
    	}
    }
    
    
    /**
     * Sets scheduled update to particular date
     */
    private synchronized void scheduleUpdate(Date nextDate)
    {
    	//don't schedule if has earlier execution
    	if(nextUpdateDate!=null && nextUpdateDate.compareTo(nextDate)<0)
    	{
        	logger.debug("ignoring next game update to for "+ nextDate + " because update is already scheduled for " + nextUpdateDate);
    		return;
    	}
    	logger.debug("next game update is scheduling to "+ nextDate);
       	nextUpdateDate = nextDate;
       	
    	scheduleTimer.cancel();
    	scheduleTimer = new Timer();
    	scheduleTimer.schedule(new TimerTask()
    	{
			@Override
			public void run() {
				synchronized (LotterySystemSimulator.this) {
					nextUpdateDate = null;
				}
				updateFinishedGames();
				updateNextSchedule();
			}
    		
    	}, nextDate);    	
    }
    
    /**
     * Sets scheduled update to the next game date
     */
    private void updateNextSchedule()
    {
    	logger.debug("looking for nearest game date");
		//select nearest raffling game date from now
        String hql = "select g.resultDate from Game g where g.winningNumbersRaw IS NULL ORDER BY g.resultDate ASC";
        //create parameters
        List<Date> nextGameDates = PersistenceManager.getInstance().selectQuery(hql, 0, 1);
        if(nextGameDates.size()>0)
        {        	
        	if(nextGameDates.size()>1) logger.warn("more than one date returned by next schedule query");
        	Date nextGameDate = nextGameDates.get(0);
        	scheduleUpdate(nextGameDate);
        }
        else
        {
        	logger.debug("no pending games to schedule in the future");
        }
    }
    


    /**
     * Creates new game and schedules for result simulation
     * @param description
     * @param participationCost
     * @param maxPrize
     * @param openDate
     * @param closeDate
     * @param resultDate
     * @return numeric id of new game or error string beginning with "Error" word 
     */
    public String addSimulatedGame(String description, 
    		float participationCost,
    		float maxPrize,
            Date openDate, 
            Date closeDate, 
            Date resultDate) 
    {
    	long gameId = (long)(Math.random()*Long.MAX_VALUE);
    	String result = lotteryHandler.addNewAvailableGame(gameId, description, participationCost, maxPrize, openDate, closeDate, resultDate);
    	if(Constants.RESULT_OK.equals(result))
    	{
        	scheduleUpdate(resultDate);
    		return Long.toString(gameId);
    	}
        return result;
    }
  
    /**
     * Returns next schedule date
     * @return
     */
    public Date getNextUpdateSchedule()
    {
    	return nextUpdateDate;
    }
    
}
